import os
import sys
import json
import time
import atexit
import ctypes
import win32api
import winerror
import keyboard
import win32event
import tkinter as tk
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver import Edge,EdgeOptions
from selenium.webdriver.edge.options import Options
from selenium.webdriver.edge.service import Service
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support import expected_conditions as EC
ctypes.windll.user32.ShowWindow(ctypes.windll.kernel32.GetConsoleWindow(), 0)#隐藏控制台dos窗口


class ChangeChannel:

    def __init__(self):
        self.check_in_service()
        self.alpha = 0.3 #默认透明度
        #获取当前用户的主目录路径拼接文件名，避免文件在C盘没有访问权限无法写入
        self.channel_file = os.path.join(os.path.expanduser("~"), "CCTV.json")
        self.channel_name_list = self.read_channel_name()
        self.channel_total = len(self.channel_name_list)
        self.channel = 0  # 存放当前要切换的频道
        self.now_channel = None #存放当前正在播放的频道
        self.input_channel = ''  # 存放输入的频道
        self.last_esc_time = 0  # 记录上次按ESC的时间
        self.alpha_taskid = None  # 存放透明度计时器id
        self.change_taskid = None #存放切换频道计时器id
        self.init_ui()
        self.init_driver()
        keyboard.on_press(self.on_key)#监听键盘

    def init_driver(self):
        options = webdriver.EdgeOptions()
        options.add_experimental_option('detach', True)#设置不自动关闭浏览器
        options.add_argument('--start-maximized')
        options.add_experimental_option("excludeSwitches", ["enable-automation"])# 隐藏"正由自动测试软件控制"提示
        service = Service(log_path="NUL")# 打包成exe运行时edge会打开dos窗口，加上这个参数可以隐藏dos窗口
        self.dr = webdriver.Edge(options=options,service=service)
        self.dr.get('https://www.yangshipin.cn/tv/home')
        #尝试自动全屏和打开声音失败则跳过，防止阻塞线程
        try:
            #等待加载元素出现，包含了等待网页加载的时间
            WebDriverWait(self.dr, 30).until(
                EC.presence_of_element_located((By.XPATH, '//*[contains(text(),"全力加载中")]')))
            #等待加载元素消失
            WebDriverWait(self.dr, 60).until(
                EC.invisibility_of_element_located((By.XPATH, '//*[contains(text(),"全力加载中")]'))
            )
            #鼠标悬停播放窗口，使声音和全屏按钮出现
            ele = self.dr.find_element(By.CLASS_NAME, 'container')
            ActionChains(self.dr).move_to_element(ele).perform()
            #打开声音只能用script方法，全屏只能用click方法，否则找不到元素(ps：得先全屏再打开声音，否则先开声音不知为何视频会暂停)
            self.dr.find_element(By.CLASS_NAME, 'full2').click()
            element = self.dr.find_elements(By.CLASS_NAME, 'voice')[0]
            self.dr.execute_script("arguments[0].click();", element)
        except:
            pass
        self.check_network()
        self.check_video_lag()
        self.check_windows_min()
        self.check_windows_close()

    def init_ui(self):
        self.root = tk.Tk()
        self.root.geometry("200x150+0+0")
        self.root.attributes('-topmost', True)  # 窗口置顶
        self.root.attributes('-alpha', self.alpha)
        self.root.overrideredirect(True)  # 禁止窗口移动，同时无法调整大小
        self.label = tk.Label(self.root, text=self.channel, font=('Arial', 120))
        self.label.pack()

    def read_channel_name(self):
        '''读取频道json,如果文件不存在则生成并写入文件'''
        try:
            with open(self.channel_file, 'r', encoding='utf-8') as f:
                data = json.load(f)
                channel_urls = list(data.keys())
                return channel_urls
        except(FileNotFoundError, json.JSONDecodeError):
            with open(self.channel_file, 'w', encoding='utf-8') as f:
                json_data = {
                    "CCTV1": "https://www.yangshipin.cn/tv/home?pid=600001859",
                    "CCTV2": "https://www.yangshipin.cn/tv/home?pid=600001800",
                    "CCTV4": "https://www.yangshipin.cn/tv/home?pid=600001814",
                    "CCTV5": "https://www.yangshipin.cn/tv/home?pid=600001818",
                    "CCTV5+": "https://www.yangshipin.cn/tv/home?pid=600001817",
                    "CCTV6": "https://www.yangshipin.cn/tv/home?pid=600108442",
                    "CCTV7": "https://www.yangshipin.cn/tv/home?pid=600004092",
                    "CCTV8": "https://www.yangshipin.cn/tv/home?pid=600001803",
                    "CCTV9": "https://www.yangshipin.cn/tv/home?pid=600004078",
                    "CCTV10": "https://www.yangshipin.cn/tv/home?pid=600001805",
                    "CCTV11": "https://www.yangshipin.cn/tv/home?pid=600001806",
                    "CCTV12": "https://www.yangshipin.cn/tv/home?pid=600001807",
                    "CCTV13": "https://www.yangshipin.cn/tv/home?pid=600001811",
                    "CCTV14": "https://www.yangshipin.cn/tv/home?pid=600001809",
                    "CCTV15": "https://www.yangshipin.cn/tv/home?pid=600001815",
                    "CCTV16": "https://www.yangshipin.cn/tv/home?pid=600098637",
                    "CCTV17": "https://www.yangshipin.cn/tv/home?pid=600001810",
                    "CCTV4K": "https://www.yangshipin.cn/tv/home?pid=600002264",
                    "CCTV8K": "https://www.yangshipin.cn/tv/home?pid=600156816",
                    "CCTV风云剧场频道": "https://www.yangshipin.cn/tv/home?pid=6000996586",
                    "CCTV第一剧场频道": "https://www.yangshipin.cn/tv/home?pid=600099655",
                    "CCTV怀旧剧场频道": "https://www.yangshipin.cn/tv/home?pid=600099620",
                    "北京卫视": "https://www.yangshipin.cn/tv/home?pid=600002309",
                    "江苏卫视": "https://www.yangshipin.cn/tv/home?pid=600002521",
                    "东方卫视": "https://www.yangshipin.cn/tv/home?pid=600002483",
                    "浙江卫视": "https://www.yangshipin.cn/tv/home?pid=600002520",
                    "湖南卫视": "https://www.yangshipin.cn/tv/home?pid=600002475",
                    "湖北卫视": "https://www.yangshipin.cn/tv/home?pid=600002508",
                    "广东卫视": "https://www.yangshipin.cn/tv/home?pid=600002485",
                    "广西卫视": "https://www.yangshipin.cn/tv/home?pid=600002509",
                    "黑龙江卫视": "https://www.yangshipin.cn/tv/home?pid=600002498",
                    "海南卫视": "https://www.yangshipin.cn/tv/home?pid=600002506",
                    "重庆卫视": "https://www.yangshipin.cn/tv/home?pid=600002531",
                    "深圳卫视": "https://www.yangshipin.cn/tv/home?pid=600002481",
                    "四川卫视": "https://www.yangshipin.cn/tv/home?pid=600002516",
                    "河南卫视": "https://www.yangshipin.cn/tv/home?pid=600002525",
                    "福建东南卫视": "https://www.yangshipin.cn/tv/home?pid=600002484",
                    "贵州卫视": "https://www.yangshipin.cn/tv/home?pid=600002490",
                    "江西卫视": "https://www.yangshipin.cn/tv/home?pid=600002503",
                    "辽宁卫视": "https://www.yangshipin.cn/tv/home?pid=600002505",
                    "安徽卫视": "https://www.yangshipin.cn/tv/home?pid=600002532",
                    "河北卫视": "https://www.yangshipin.cn/tv/home?pid=600002493",
                    "山东卫视": "https://www.yangshipin.cn/tv/home?pid=600002513",
                    "天津卫视": "https://www.yangshipin.cn/tv/home?pid=600152137",
                    "吉林卫视": "https://www.yangshipin.cn/tv/home?pid=600190405",
                    "陕西卫视": "https://www.yangshipin.cn/tv/home?pid=600190400",
                    "甘肃卫视": "https://www.yangshipin.cn/tv/home?pid=600190408",
                    "宁夏卫视": "https://www.yangshipin.cn/tv/home?pid=600190737",
                    "内蒙古卫视": "https://www.yangshipin.cn/tv/home?pid=600190401",
                    "云南卫视": "https://www.yangshipin.cn/tv/home?pid=600190402",
                    "山西卫视": "https://www.yangshipin.cn/tv/home?pid=600190407",
                    "青海卫视": "https://www.yangshipin.cn/tv/home?pid=600190406",
                    "西藏卫视": "https://www.yangshipin.cn/tv/home?pid=600190403",
                    "中国教育电视台1频道": "https://www.yangshipin.cn/tv/home?pid=600171827",
                    "新疆卫视": "https://www.yangshipin.cn/tv/home?pid=600152138"
                }
                json.dump(json_data, f, ensure_ascii=False, indent=4)
            return self.read_channel_name()

    def set_alpha(self, alpha):
        '''设置频道窗口透明度'''
        self.root.attributes('-alpha', alpha)

    def change_channel(self):
        #如果切换的频道和当前播放频道不同才会切换
        if self.now_channel != self.channel:
            channel_name = self.channel_name_list[self.channel]
            #通过频道名模糊匹配来定位元素并点击切换频道
            element = self.dr.find_elements(By.XPATH,f'//*[contains(text(),"{channel_name}")]')
            self.dr.execute_script("arguments[0].click();", element[0])
        self.input_channel = ''  # 每次切换频道后置空输入的频道
        self.now_channel = self.channel #更新当前正在播放频道为最新切换的频道


    def on_key(self, event):
        '''监听按钮，左右箭头切换频道，空格和enter确定，'''
        self.reset_alpha_taskid()
        # 双击esc关闭窗口不用判断窗口是否最小化所以不放在下面if中，判断2次点击时间间隔是否在1秒内
        if event.name == 'esc':
            now = time.time()
            if now - self.last_esc_time < 1:
                #TODO:quit会导致线程阻塞，用close替代，但是close只能关闭当前页面，当有多个页面时无法直接关闭浏览器
                self.dr.close()
                self.root.destroy()
            self.last_esc_time = now
        #每次按键检测窗口是否最小化，非最小化才执行操作，最小化则跳过操作
        if not self.check_windows_min():
            channle_num = None
            self.reset_change_taskid()  #每次输入都重置定时任务
            # 当输入是数字，且输入频道>=频道总数时将输入置空，下次输入又从个位开始实现循环输入
            if event.name.isdigit():
                if len(self.input_channel) >= len(str(self.channel_total)):
                    self.input_channel = ''
                channle_num = event.name
            elif event.name == 'left':
                channle_num = 1
            elif event.name == 'right':
                channle_num = -1
            # 当频道有变化时点击空格和回车是立即切换频道，且手动确定后取消自动切换频道任务
            elif event.name in ['space','enter']:
                if self.input_channel:
                    self.change_channel()
                    self.root.after_cancel(self.change_taskid)
            #频道有改变则更新窗口显示最新频道数，通过箭头切换立即生效无需等待时间
            if channle_num:
                self.update_channle_label(channle_num)
                if event.name in ['left', 'right']:
                    self.change_channel()

    def update_channle_label(self, channel_num):
        '''更新频道标签，输入传入的字符串格式，上下切换时整数格式'''
        # 上下按钮切换频道通过模运算来限制频道只能在0-channel_total之间，实现频道循环
        if isinstance(channel_num, int):
            self.channel = (self.channel + channel_num) % self.channel_total
        #通过min对比后取最小值，控制输入频道不超过总频道
        elif isinstance(channel_num, str):
            self.input_channel = min(self.input_channel+channel_num, str(self.channel_total-1),key=int)
            self.channel = int(self.input_channel)#更新当前频道为输入的频道
        self.set_alpha(1)#切换频道时使频道窗口不透明
        self.label.config(text=self.channel)

    def reset_change_taskid(self):
        '''创建x秒后切换频道任务，该时间内调用则取消原任务，并创建新的任务'''
        if self.change_taskid:
            self.root.after_cancel(self.change_taskid)
        self.change_taskid = self.root.after(2000, self.change_channel)

    def reset_alpha_taskid(self):
        '''创建x秒后设置频道窗口透明度，该时间内调用则取消原任务，并创建新的任务'''
        if self.alpha_taskid:
            self.root.after_cancel(self.alpha_taskid)
        self.alpha_taskid = self.root.after(2000, lambda :self.set_alpha(self.alpha))

    def check_windows_close(self):
        '''通过获取窗口句柄来判断浏览器是否关闭，关闭则同时关闭频道窗口,每1秒检查一次'''
        try:
            self.dr.window_handles
        except:
            self.root.destroy()
        self.root.after(1000, lambda : self.check_windows_close())

    def check_windows_min(self):
        '''检查浏览器是否最小化,最小化时隐藏窗口'''
        is_min = self.dr.execute_script('return document.hidden')
        if is_min:
            self.root.withdraw()#隐藏窗口
        else:
            self.root.deiconify()#恢复窗口
        self.root.after(1000, lambda: self.check_windows_min())
        return is_min

    def check_network(self):
        '''监听是否出现网异常情况，当出现点击重试时刷新页面'''
        try:
            element = WebDriverWait(self.dr, 1).until(
                EC.presence_of_element_located((By.XPATH, "//*[contains(text(),'点击重试')]"))
            )
            print()
            self.dr.refresh()
        except TimeoutException:
            pass
        self.root.after(1000, lambda : self.check_network())

    def check_video_lag(self):
        '''监听视频是否卡顿（暂停不算卡顿），卡顿时切换视频清晰度'''
        js = """
            var video = document.querySelector('video');
            if (!video) return false;
            // 初始化 lastVideoTime
            if (typeof window.lastVideoTime === 'undefined') {
                window.lastVideoTime = video.currentTime;
                return false;
            }
            // 卡顿条件：时间未变化 + 视频正在播放（排除暂停状态）
            var is_stuck = (Math.abs(video.currentTime - window.lastVideoTime) < 0.1) && !video.paused;
            window.lastVideoTime = video.currentTime;
            return is_stuck;
        """
        if self.dr.execute_script(js):
            self.switch_dpi()
        self.root.after(1000, lambda : self.check_video_lag())

    def switch_dpi(self, value='540P'):
        '''切换视频清晰度(1080P,720P,540P)，如果失败则跳过防止线程阻塞'''
        try:
            #鼠标悬停播放窗口使按钮出现
            ele = self.dr.find_element(By.CLASS_NAME, 'container')
            ActionChains(self.dr).move_to_element(ele).perform()
            #鼠标悬停当前清晰度并点击切换清晰度
            ele = self.dr.find_elements(By.XPATH,"//*[contains(text(),'蓝光') or contains(text(),'超清') or contains(text(),'高清')]")[1]
            ActionChains(self.dr).move_to_element(ele).perform()
            self.dr.find_element(By.XPATH, f"//*[contains(text(),'{value}')]").click()
        except:
            pass

    def check_in_service(self):
        '''检测程序是否在运行中，运行中则不再启动防止重复运行'''
        mutex_name = "Global\\MySeleniumEdgeAutomationMutex"  # 全局唯一名称
        mutex = win32event.CreateMutex(None, True, mutex_name)  # True表示立即获取所有权
        if win32api.GetLastError() == winerror.ERROR_ALREADY_EXISTS:
            sys.exit(1)# 非零状态码表示异常退出
        atexit.register(lambda: win32api.CloseHandle(mutex))

if __name__ == '__main__':
    change = ChangeChannel()
    change.root.mainloop()